﻿namespace Webinator.Impls.Webdriver
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Linq;
    using System.Threading;
    using Enums;
    using ExtensionMethods;
    using Interfaces;
    using OpenQA.Selenium;
    using Toolboxes;

    [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed. Suppression is OK here.")]
    internal class WebDriverCore : ICore
    {
        private const int DefaultPollRate = 250;

        private const int DefaultTimeout = 20000;

        public IWebDriver Driver;

        private CoreCommon _common;

        private string _mainHandle;

        private string _currentHandle;

        public WebDriverCore(IWebDriver driver, Config config)
        {
            // When the driver changes, update it via UpdateCore()
            Driver = driver;
            _common = new CoreCommon();
            Config = config;
        }

        public Config Config { get; set; }

        public void AddCookie(string name, string value, string path, string domain)
        {
            // This could fail on IE if protected mode is on.
            Driver.Manage().Cookies.AddCookie(new Cookie(name, value, domain, path, DateTime.MaxValue));
        }

        public void CloseBrowser()
        {
            try
            {
                // Chrome requires Quit() or else the command window is left open
                // RemoteWebDriver requires a Quit() as well to release the grid resource
                if (Config.Browser == Config.AvailableBrowsers.Chrome || Config.Framework == Config.AvailableFrameworks.WebDriverRemote)
                {
                    Driver.Quit();
                }
                else
                {
                    Driver.Close();
                }
            }
            catch
            {
            }
        }

        public bool DialogPresent()
        {
            try
            {
                Driver.SwitchTo().Alert();
            }
            catch (NoAlertPresentException)
            {
                return false;
            }

            return true;
        }

        public void ConfirmDialog()
        {
            Driver.SwitchTo().Alert().Accept();
        }

        public void DeleteCookie(string name)
        {
            // This could fail on IE if protected mode is on.
            Driver.Manage().Cookies.DeleteCookieNamed(name);
        }

        public void DismissDialog()
        {
            Driver.SwitchTo().Alert().Dismiss();
        }

        public void ElementClear(object element)
        {
            var e = (IWebElement)element;
            CoreCommon.SafeAction(e, e.Clear);
        }

        public void ElementClick(object element)
        {
            var e = (IWebElement)element;
            CoreCommon.SafeAction(e, e.Click);
        }

        public void ElementFocus(object element)
        {
            ((IJavaScriptExecutor)Driver).ExecuteScript("arguments[0].focus();", element);
        }

        public bool ElementIsChecked(object element)
        {
            return ((IWebElement)element).Selected;
        }

        public bool ElementIsDisplayed(object element)
        {
            return ((IWebElement)element).Displayed;
        }

        public void ElementSelect(object element)
        {
            var e = (IWebElement)element;
            CoreCommon.SafeAction(e, e.Click);
        }

        public void ElementSendkeys(object element, string text)
        {
            var e = (IWebElement)element;
            ElementClear(element);

            if (!string.IsNullOrEmpty(text))
            {
                CoreCommon.SafeAction(e, () => e.SendKeys(text));
            }
        }

        public void ElementTriggerEvent(object element, EventIs eventIs)
        {
            // No native method
            var myEvent = eventIs.ToString().ToLower();
            InjectJquery();

            ((IJavaScriptExecutor)Driver).ExecuteScript(eventIs == EventIs.RightClick
                ? "$(arguments[0]).trigger({type: 'mousedown', which: 3});"
                : string.Format("$(arguments[0]).trigger('{0}');", myEvent), element);
        }

        public object EvaluateScript(string js)
        {
            return ((IJavaScriptExecutor)Driver).ExecuteScript(js);
        }

        public void ExecuteScript(string js)
        {
            ((IJavaScriptExecutor)Driver).ExecuteScript(js);
        }

        public object FindElement(Locator locator, int timeout = DefaultTimeout)
        {
            var by = GetByFromLocator(locator);
            var element = WaitForElement(by, locator.Index, timeout);
            if (element == null)
            {
                throw new Exception("Cannot find element at: " + by);
            }

            return element;
        }

        public object FindElements(Locator locator)
        {
            var by = GetByFromLocator(locator);
            var elements = FindElementsBy(by);
            if (elements == null)
            {
                throw new Exception("Cannot find elements at: " + by);
            }

            return elements;
        }

        public string GetAttribute(object element, string attribute)
        {
            var e = (IWebElement)element;
            return CoreCommon.SafeAction(e, () => e.GetAttribute(attribute)) ?? string.Empty;
        }

        public object GetCore()
        {
            return Driver;
        }

        public string GetElementText(object element)
        {
            return ((IWebElement)element).Text;
        }

        public Rectangle GetLocatorLocation(Locator locator)
        {
            string javascript = @"var r=arguments[0].getBoundingClientRect();return r.left + ' ' + r.top + ' ' + r.width + ' ' + r.height;";

            var element = (IWebElement)FindElement(locator);
            var obj = (string)((IJavaScriptExecutor)Driver).ExecuteScript(javascript, element);
            var vals = obj.Split(' ');
            var location = new Rectangle(
                (int)double.Parse(vals[0]),
                (int)double.Parse(vals[1]),
                (int)double.Parse(vals[2]),
                (int)double.Parse(vals[3]));

            return location;
        }

        public IEnumerable<Rectangle> GetLocatorLocations(Locator locator)
        {
            List<Rectangle> locations = new List<Rectangle>();
            string javascript = @"var r=arguments[0].getBoundingClientRect();return r.left + ' ' + r.top + ' ' + r.width + ' ' + r.height;";

            var elements = (ReadOnlyCollection<IWebElement>)FindElements(locator);
            foreach (IWebElement element in elements)
            {
                var obj = (string)((IJavaScriptExecutor)Driver).ExecuteScript(javascript, element);
                var vals = obj.Split(' ');
                var location = new Rectangle(
                    (int)double.Parse(vals[0]),
                    (int)double.Parse(vals[1]),
                    (int)double.Parse(vals[2]),
                    (int)double.Parse(vals[3]));
                locations.Add(location);
            }

            return locations;
        }

        public string GetPageSource()
        {
            string html = null;
            int i = 0;
            bool success;
            do
            {
                try
                {
                    html = Driver.PageSource;
                    success = true;
                }
                catch (InvalidOperationException)
                {
                    success = false;
                }

                if (!success)
                {
                    Thread.Sleep(100);
                }

                i++;
            }
            while (!success && i < 5);

            return html;
        }

        public string GetPageTitle()
        {
            return Driver.Title;
        }

        public string GetPageUrl()
        {
            return Driver.Url;
        }

        public SelectedItem GetSelectedItem(Locator locator)
        {
            var element = FindElement(locator);

            var selectedItem = new SelectedItem
            {
                Text = (string)((IJavaScriptExecutor)Driver).ExecuteScript("return arguments[0].options[arguments[0].selectedIndex].text;", element),
                Value = (string)((IJavaScriptExecutor)Driver).ExecuteScript("return arguments[0].options[arguments[0].selectedIndex].value;", element),
                Index = (long)((IJavaScriptExecutor)Driver).ExecuteScript("return arguments[0].selectedIndex;", element)
            };

            return selectedItem;
        }

        public IEnumerable<SelectedItem> GetSelectedItems(Locator locator)
        {
            var element = FindElement(locator);
            var obj = (ReadOnlyCollection<object>)((IJavaScriptExecutor)Driver).ExecuteScript("{var e = arguments[0]; var selected = new Array(); var index = 0; for (var i=0; i< e.length; i++) { if (e[i].selected) { index = selected.length; selected[index] = new Object; selected[index].value = e[i].value; selected[index].index = i; selected[index].text = e[i].text; } } return selected;}", element);

            var selectedItems = new List<SelectedItem>();
            foreach (object o in obj)
            {
                var item = (Dictionary<string, object>)o;
                var selectedItem = new SelectedItem();
                selectedItem.Text = item["text"].ToString();
                selectedItem.Value = item["value"].ToString();
                selectedItem.Index = (int)double.Parse(item["index"].ToString());
                selectedItems.Add(selectedItem);
            }

            return selectedItems;
        }

        public string GetVisibleText()
        {
            InjectJquery();
            var text = (string)EvaluateScript("return $('body').children(':visible').text();");
            return text;
        }

        public void NavigateBack()
        {
            Driver.Navigate().Back();
        }

        public void NavigateForward()
        {
            Driver.Navigate().Forward();
        }

        public void NavigateRefresh()
        {
            Driver.Navigate().Refresh();
        }

        public void NavigateToUrl(string url)
        {
            Driver.Url = url;
        }

        public bool QuickElementExists(Locator locator)
        {
            var compiledXpath = XPathToolbox.GetXpathFromLocator(locator);
            return _common.HtmlContainsXpath(compiledXpath, GetPageSource());
        }

        public bool QuickWaitElementDoesntExist(Locator locator, int waitInMilliseconds)
        {
            return _common.QuickWait(locator, waitInMilliseconds, false, GetPageSource);
        }

        public bool QuickWaitElementExists(Locator locator, int waitInMilliseconds)
        {
            return _common.QuickWait(locator, waitInMilliseconds, true, GetPageSource);
        }

        public void SelectListItems(Locator locator, Selector[] selectors)
        {
            var xpath = string.Format("({0})//option", XPathToolbox.GetXpathFromLocator(locator));
            var elements = selectors.Select(selector => (IWebElement)FindElement(GetListItem(selector, xpath))).ToList();

            if (elements.Count == 1)
            {
                ElementClick(elements.First());
            }
            else
            {
                if (Config.UseJavascript)
                {
                    foreach (var element in elements)
                    {
                        ((IJavaScriptExecutor)Driver).ExecuteScript("arguments[0].selected = true;", element);
                    }
                }
                else
                {
                    elements.ForEach(e => e.Click());
                }
            }
        }

        public void SetAttribute(object element, string attribute, string value)
        {
            // JQuery is the only way to do this reliably - javascript would make this function massive.
            InjectJquery();
            CoreCommon.SafeAction(element, () => ((IJavaScriptExecutor)Driver).ExecuteScript("$(arguments[0]).attr(arguments[1], arguments[2]);", element, attribute, value));
        }

        public void UpdateCore(object driver)
        {
            Driver = (IWebDriver)driver;
        }

        public bool WaitElementAppear(Locator locator, int waitInMilliseconds, int index = 0)
        {
            return WaitForElement(GetByFromLocator(locator), index, waitInMilliseconds) != null;
        }

        public bool WaitElementDisappear(Locator locator, int waitInMilliseconds, int index = 0)
        {
            var y = waitInMilliseconds / DefaultPollRate;

            for (var x = 0; x < y; x++)
            {
                if (!WaitElementAppear(locator, DefaultPollRate, index))
                {
                    return true;
                }

                Thread.Sleep(DefaultPollRate);
            }

            return false;
        }

        public void SwitchWindow(SwitchBy switchBy, Comparison comparison, string argument)
        {
            var handles = Driver.WindowHandles;
            var currentHandle = Driver.CurrentWindowHandle;

            if (string.IsNullOrEmpty(_mainHandle))
            {
                _mainHandle = currentHandle;
            }

            var windowFound = false;
            foreach (var handle in handles.Where(handle => handle != _mainHandle && handle != currentHandle))
            {
                Driver.SwitchTo().Window(handle);

                string value = null;
                switch (switchBy)
                {
                    case SwitchBy.Url:
                        value = Driver.Url;
                        break;
                    case SwitchBy.Title:
                        value = Driver.Title;
                        break;
                }

                if (value.StringMatch(comparison, argument))
                {
                    windowFound = true;
                    _currentHandle = handle;
                    //Console.WriteLine("Switch to Window: {0} From: {1}", handle, currentHandle);
                }
            }

            if (!windowFound)
            {
                Driver.SwitchTo().Window(currentHandle);
                throw new NoSuchWindowException(string.Format("FAILED: Switching to window by: [{0}:{1}]", switchBy, argument));                
            }
        }

        public void SwitchToMainWindow()
        {
            Driver.SwitchTo().Window(_mainHandle);
            _currentHandle = _mainHandle;
        }

        public void SwitchToMainFrame()
        {
            Driver.SwitchTo().DefaultContent();
        }

        public void SwitchToFrame(Locator locator)
        {
            var element = (IWebElement)FindElement(locator);
            Driver.SwitchTo().Frame(element);
        }

        public bool CurrentWindowIsValid()
        {
            bool valid = true;

            var t = new Thread(() =>
            {
                try
                {
                    if (string.IsNullOrEmpty(_currentHandle))
                    {
                        var temp = Driver.CurrentWindowHandle;
                        //Console.WriteLine("Current handle: {0}", temp);
                    }
                    else
                    {
                        valid = Driver.WindowHandles.Contains(_currentHandle);
                        //Console.WriteLine("Current handle: {0}", _currentHandle);
                        //Console.WriteLine("Handles: {0}", string.Join(" | ", Driver.WindowHandles));
                    }
                }
                catch
                {
                    valid = false;
                }
            });

            var sw = Stopwatch.StartNew();
            t.Start();

            do
            {
                if (!t.IsAlive) break;
                Thread.Sleep(50);
            }
            while (sw.ElapsedMilliseconds <= 500);
                
            sw.Stop();
                
            if (t.IsAlive)
            {
                t.Abort();
            }

            //Console.WriteLine("Current Window Valid: {0}", valid);
            
            return valid;
        }

        public void CloseWindow()
        {
            if (string.IsNullOrEmpty(_mainHandle) || Driver.CurrentWindowHandle == _mainHandle)
            {
                return;
            }

            Driver.Close();
            Driver.SwitchTo().Window(_mainHandle);
        }

        public IEnumerable<string> WindowTitles()
        {
            var handles = Driver.WindowHandles;
            var currentHandle = Driver.CurrentWindowHandle;
            var titles = new List<string>();

            foreach (var handle in handles)
            {
                Driver.SwitchTo().Window(handle);
                titles.Add(Driver.Title);
            }

            Driver.SwitchTo().Window(currentHandle);

            return titles;
        }

        public IEnumerable<string> WindowUrls()
        {
            var handles = Driver.WindowHandles;
            var currentHandle = Driver.CurrentWindowHandle;
            var urls = new List<string>();

            foreach (var handle in handles)
            {
                Driver.SwitchTo().Window(handle);
                urls.Add(Driver.Url);
            }

            Driver.SwitchTo().Window(currentHandle);

            return urls;
        }

        private ReadOnlyCollection<IWebElement> FindElementsBy(By by)
        {
            while (true)
            {
                try
                {
                    return Driver.FindElements(by);
                }
                catch
                {
                    return null;
                }
            }
        }

        private By GetByFromLocator(Locator locator)
        {
            switch (locator.FindBy)
            {
                case FindBy.Id:
                    return locator.IsPartial
                               ? By.XPath(XPathToolbox.GetXpathFromLocator(locator, true))
                               : By.Id(locator.Value);
                case FindBy.Name:
                    return locator.IsPartial
                               ? By.XPath(XPathToolbox.GetXpathFromLocator(locator, true))
                               : By.Name(locator.Value);
                case FindBy.Text:
                    return locator.IsPartial
                               ? By.PartialLinkText(locator.Value)
                               : By.LinkText(locator.Value);
                case FindBy.Class:
                    if (locator.Value.Contains(" ") || locator.IsPartial)
                    {
                        return By.XPath(XPathToolbox.GetXpathFromLocator(locator, true));
                    }

                    return By.ClassName(locator.Value);
                case FindBy.Css:
                    return By.CssSelector(locator.Value);
                case FindBy.Value:
                case FindBy.Href:
                case FindBy.Alt:
                case FindBy.XPath:
                    return By.XPath(XPathToolbox.GetXpathFromLocator(locator, true));
                default:
                    throw new Exception("No By implemented for : " + locator.FindBy);
            }
        }

        private IWebElement GetElement(By by, int index)
        {
            var elements = FindElementsBy(by);

            if (elements == null || elements.Count == 0 || index >= elements.Count)
            {
                return null;
            }

            return elements[index];
        }

        private Locator GetListItem(Selector selector, string xpath)
        {
            Locator item;
            switch (selector.SelectorIs)
            {
                case SelectorIs.Text:
                    item = new Locator(FindBy.XPath, string.Format("{0}[normalize-space(text())={1}]", xpath, XPathToolbox.XPathLiteral(selector.Argument)));
                    break;
                case SelectorIs.PartialText:
                    item = new Locator(FindBy.XPath, string.Format("{0}[contains(normalize-space(text()),{1})]", xpath, XPathToolbox.XPathLiteral(selector.Argument)));
                    break;
                case SelectorIs.Value:
                    item = new Locator(FindBy.XPath, string.Format("{0}[@value={1}]", xpath, XPathToolbox.XPathLiteral(selector.Argument)));
                    break;
                case SelectorIs.PartialValue:
                    item = new Locator(FindBy.XPath, string.Format("{0}[contains(@value,{1})]", xpath, XPathToolbox.XPathLiteral(selector.Argument)));
                    break;
                case SelectorIs.Index:
                    item = new Locator(FindBy.XPath, string.Format("({0})[{1}]", xpath, int.Parse(selector.Argument) + 1));
                    break;
                default:
                    throw new ArgumentOutOfRangeException(string.Format("SelectBy [{0}] not implemented.", selector.Argument));
            }

            return item;
        }

        private void InjectJquery()
        {
            ExecuteScript(Scripts.JQuery);
        }

        private IWebElement WaitForElement(By by, int index = 0, int timeout = DefaultTimeout)
        {
            var y = timeout / DefaultPollRate;
            var sw = new Stopwatch();
            sw.Start();

            for (var x = 0; x < y; x++)
            {
                if (sw.ElapsedMilliseconds > timeout)
                {
                    return null;
                }

                try
                {
                    var element = GetElement(by, index);
                    if (element != null)
                    {
                        return element;
                    }
                }
                catch
                {
                    // Don't throw an error, just return null
                }

                Thread.Sleep(DefaultPollRate);
            }

            return null;
        }
    }
}